﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace LPLib
{
    /// <summary>
    /// Provides a container for Polymers.
    /// </summary>
    /// <typeparam name="T">The type used to encode nucleotides.</typeparam>
    public class PolymerCollection<T> : IEnumerable
    {
        MonomerEncoding<T> monomerEncoding;
        Reader<T> reader;

        /// <summary>
        /// The name used to identify the collection.
        /// </summary>
        public string Name { get; set; }

        protected int maxLength;
        /// <summary>
        /// The length of the longest sequence in the collection.
        /// </summary>
        public int MaxLength
        {
            get { return maxLength; }
        }

        protected Dictionary<string, Polymer<T>> members;

        /// <summary>
        /// Default constructor.
        /// </summary>
        public PolymerCollection(MonomerEncoding<T> encoding)
        {
            this.monomerEncoding = encoding;
            this.reader = new Reader<T>(encoding);
            Name = "NULL";
            members = new Dictionary<string, Polymer<T>>();
            maxLength = 0;
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="_members">A list containing the polynucleotides of the appropriate type to be stored in this instance.</param>
        public PolymerCollection(MonomerEncoding<T> encoding, List<Polymer<T>> _members)
        {
            this.monomerEncoding = encoding;
            this.reader = new Reader<T>(encoding);
            Name = "NULL";
            members = new Dictionary<string, Polymer<T>>();
            for (int i = 0; i < _members.Count; i++)
            {
                members.Add(_members[i].Name, _members[i]);
            }
            maxLength = getMaxLength(this);
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="_members">A list containing the polynucleotides of the appropriate type to be stored in this instance.</param>
        public PolymerCollection(MonomerEncoding<T> encoding, string dataString, SequenceFileFormat format = SequenceFileFormat.FASTA)
        {
            this.monomerEncoding = encoding;
            this.reader = new Reader<T>(encoding);
            Name = "NULL";
            members = new Dictionary<string, Polymer<T>>();
            switch (format)
            {
                case SequenceFileFormat.FASTA:
                    FromFASTA(dataString);
                    break;
                case SequenceFileFormat.FASTQ:
                    FromFASTQ(dataString);
                    break;
            }
        }

        /// <summary>
        /// Accessor for members of the collection.
        /// </summary>
        /// <param name="name">The name of the Polynuc desired.</param>
        /// <returns>The Polynuc of interest.</returns>
        public Polymer<T> this[string name]
        {
            get { return members[name]; }
        }

        /// <summary>
        /// Determines whether the collection contains a polynucleotide with a given name.
        /// </summary>
        /// <param name="name">The name to seek.</param>
        /// <returns>True if the name is the name of one of the polynucleotides. False if not.</returns>
        public bool Contains(string name)
        {
            return members.ContainsKey(name);
        }

        /// <summary>
        /// Renames a polynucleotide in the collection.
        /// </summary>
        /// <param name="name">The original name.</param>
        /// <param name="newName">The new name to replace the original one. Throws an argument exception if the 
        /// original name is contained in the collection, or if the new name is already contained in the collection.</param>
        public void Rename(string name, string newName)
        {
            if (newName == name)
                return;

            if (members.ContainsKey(name))
            {
                if (!members.ContainsKey(newName))
                {
                    Polymer<T> p = members[name];
                    p.Name = newName;
                    members.Add(newName, p);
                    members.Remove(name);
                }
                else
                {
                    throw new ArgumentException("The new name duplicates an existing name.");
                }
            }
            else
            {
                throw new KeyNotFoundException("No item named " + name + " was found.");
            }
        }

        /// <summary>
        /// Renames a polynucleotide in the collection.
        /// </summary>
        /// <param name="name">The original name.</param>
        /// <param name="newName">The new name to replace the original one. Throws an argument exception if the 
        /// original name is contained in the collection, or if the new name is already contained in the collection.</param>
        public void Rename(Dictionary<string, string> renameKey)
        {

            foreach (KeyValuePair<string, string> kvp in renameKey)
            {
                if (!members.ContainsKey(kvp.Key))
                {
                    throw new ArgumentException("The name " + kvp.Key + " does not exist.");
                }

                if (members.ContainsKey(kvp.Value))
                {
                    if (kvp.Key != kvp.Value)
                        throw new ArgumentException("The name " + kvp.Value + " already exists.");
                }
            }

            foreach (string originalName in renameKey.Keys)
            {
                if (renameKey[originalName] == originalName)
                    continue;

                Polymer<T> p = members[originalName];
                p.Name = renameKey[originalName];
                members.Add(p.Name, p);
                members.Remove(originalName);
            }
        }

        /// <summary>
        /// Adds an ID/Polynuc vpair to the collection. Updates the MaxLength properties as necessary.
        /// </summary>
        /// <param name="p">The polynucleotide to add.</param>
        public void AddNewPolymer(string name, string sequence, byte[] qualityScores = null)
        {
            if (members.ContainsKey(name))
            {
                throw new ArgumentException("A polymer with the same name is already present.");
            }

            members.Add(name, new Polymer<T>(monomerEncoding, name, sequence, qualityScores));
            if (members.Count == 1)
            {
                maxLength = sequence.Length;
            }
            else if (sequence.Length > maxLength)
            {
                maxLength = sequence.Length;
            }
        }

        /// <summary>
        /// Adds an ID/Polynuc vpair to the collection. Updates the MaxLength properties as necessary.
        /// </summary>
        /// <param name="p">The polynucleotide to add.</param>
        public void AddMember(Polymer<T> polymer)
        {
            // note that we are not checking for consistency of encoding
            if (members.ContainsKey(polymer.Name))
            {
                throw new ArgumentException("A polymer with the same name is already present.");
            }

            members.Add(polymer.Name, polymer);
            if (members.Count == 1)
            {
                maxLength = polymer.Seq.Length;
            }
            else if (polymer.Seq.Length > maxLength)
            {
                maxLength = polymer.Seq.Length;
            }
        }

        /// <summary>
        /// Removes the named polynucleotide from the collection. Updates the MaxLength property as necessary.
        /// </summary>
        /// <param name="name">The name of the polynucleotide to remove. Throws an exception if the name is not present in the collection.</param>
        /// <returns>True if the named polynucleotide was present; false otherwise.</returns>
        public bool RemoveMember(string name)
        {
            if (!members.ContainsKey(name))
            {
                return false;
            }
            int thislength = members[name].Seq.Length;
            members.Remove(name);
            if (thislength == maxLength)
            {
                maxLength = getMaxLength(this);
            }
            return true;
        }

        /// <summary>
        /// Removes several polynucleotides at once.
        /// </summary>
        /// <param name="toRemove">A list of names of polynucleotides to remove.</param>
        /// <returns>True if and only if all named polynucleotides were present.</returns>
        public bool RemoveMembers(List<string> toRemove)
        {
            bool allpresent = true;
            bool recomputeMaxLength = false;
            foreach (string name in toRemove)
            {
                if (!members.ContainsKey(name))
                {
                    allpresent = false;
                }
                else
                {
                    if (members[name].Seq.Length == maxLength)
                    {
                        recomputeMaxLength = true;
                    }

                    members.Remove(name);
                }
            }

            if (recomputeMaxLength)
            {
                maxLength = getMaxLength(this);
            }

            return allpresent;
        }

        /// <summary>
        /// Gets the number of polynucs stored in the instance. Read only.
        /// </summary>
        public int Count
        {
            get { return members.Count; }
        }

        /// <summary>
        /// Implements the GetEnumerator method for the IEnumerable interface.
        /// </summary>
        /// <returns></returns>
        public IEnumerator GetEnumerator()
        {
            return new PolymerEnum<T>(members);
        }

        private static int getMaxLength(PolymerCollection<T> pc)
        {
            int length = 0;
            foreach (Polymer<T> p in pc)
            {
                if (p.Seq.Length > length)
                {
                    length = p.Seq.Length;
                }
            }
            return length;
        }

        private static bool getHasUniformLength(PolymerCollection<T> pc)
        {
            if (pc.Count < 1) return true;
            int len = pc.members.First().Value.Seq.Length;
            foreach (Polymer<T> p in pc)
            {
                if (p.Seq.Length != len)
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Determines whether all of the polynucleotides in are the same length.
        /// </summary>
        /// <returns>True if and only if all the polynucleotides in this instance have the same length.</returns>
        public bool HasUniformLength()
        {
            return getHasUniformLength(this);
        }

        /// <summary>
        /// Gets a list of the names of all the polynucleotides contained in the collection.
        /// </summary>
        /// <returns>A list of names of the collection members.</returns>
        public List<string> GetNames()
        {
            List<string> names = new List<string>();
            foreach (Polymer<T> p in members.Values)
            {
                names.Add(p.Name);
            }
            return names;
        }

        public void FromFASTA(string fastaString)
        {
            members = reader.ReadFASTA(fastaString);
            maxLength = getMaxLength(this);
        }

        public void FromFASTQ(string fastqString)
        {
            members = reader.ReadFASTQ(fastqString);
            maxLength = getMaxLength(this);
        }
    }

    /// <summary>
    /// Provides an enumerator so that the "foreach" construction can be used with the PolynucCollection
    /// </summary>
    public class PolymerEnum<T> : IEnumerator
    {
        private Dictionary<string, Polymer<T>>.Enumerator enumerator;
        private Dictionary<string, Polymer<T>> members;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="_members">Pass the members field of the PolynucCollection being enumerated.</param>
        public PolymerEnum(Dictionary<string, Polymer<T>> _members)
        {
            members = _members;
            enumerator = members.GetEnumerator();
        }

        /// <summary>
        /// Advances to the next item in the collection.
        /// </summary>
        /// <returns>The enumerator positioned at the next item.</returns>
        public bool MoveNext()
        {
            return enumerator.MoveNext();
        }

        /// <summary>
        /// Empty method included to implement the interface.
        /// </summary>
        public void Reset() { }

        object IEnumerator.Current
        {
            get
            {
                return Current;
            }
        }

        /// <summary>
        /// Gets the current item.
        /// </summary>
        public Polymer<T> Current
        {
            get
            {
                try
                {
                    return enumerator.Current.Value;
                }
                catch (IndexOutOfRangeException)
                {
                    throw new InvalidOperationException();
                }
            }
        }
    }
}
